import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;
import joe.AudioMini;
import joe.Engine;
import joe.InputGlfw;
import joe.Mat4;
import joe.Render;
import joe.RenderGles3;
import joe.ResourceMapFile;
import joe.ResourceVideo;
import joe.UnsafeUtil;
import joe.Vec4;
import org.jetbrains.annotations.NotNull;
import org.lwjgl.stb.STBTTFontinfo;
import org.lwjgl.system.MemoryUtil;
import org.tinylog.Logger;
import static org.lwjgl.stb.STBTruetype.*;

// java: -XX:+UnlockExperimentalVMOptions -XX:+EnableJVMCI --enable-native-access=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED
@SuppressWarnings("unused")
public class TestJoe {
	public static void printVec4(@NotNull Vec4 v) {
		System.out.println("-------------------------------------------------------");
		System.out.println(v);
		System.out.println("-------------------------------------------------------");
	}

	public static void printMat4(@NotNull Mat4 m) {
		System.out.println("-------------------------------------------------------");
		System.out.print(m);
		System.out.println("-------------------------------------------------------");
	}

	public static boolean equalsMat4(@NotNull Mat4 m0, @NotNull Mat4 m1) {
		for (int i = 0; i < 16; i++) {
			if (m0.get(i) != m1.get(i))
				return false;
		}
		return true;
	}

	public static boolean equalsVec4(@NotNull Vec4 v0, @NotNull Vec4 v1) {
		for (int i = 0; i < 4; i++) {
			if (v0.get(i) != v1.get(i))
				return false;
		}
		return true;
	}

	// | 0 1 2 3 |
	// | 4 5 6 7 |
	// | 8 9 A B | m(mat4) * M(mat4) => r(mat4)
	// | C D E F | r[0] = dot(m[0,1,2,3], M[0,4,8,C]); ...
	public static void matMulJava(@NotNull Mat4 m, @NotNull Mat4 M, @NotNull Mat4 r) {
		float f0, f1, f2, f3;
		//@formatter:off
		f0 = m.get(0); f1 = m.get(1); f2 = m.get(2); f3 = m.get(3);
		r.set( 0, f0 * M.get( 0) + f1 * M.get(4) + f2 * M.get( 8) + f3 * M.get(12));
		r.set( 1, f0 * M.get( 1) + f1 * M.get(5) + f2 * M.get( 9) + f3 * M.get(13));
		r.set( 2, f0 * M.get( 2) + f1 * M.get(6) + f2 * M.get(10) + f3 * M.get(14));
		r.set( 3, f0 * M.get( 3) + f1 * M.get(7) + f2 * M.get(11) + f3 * M.get(15));
		f0 = m.get(4); f1 = m.get(5); f2 = m.get(6); f3 = m.get(7);
		r.set( 4, f0 * M.get( 0) + f1 * M.get(4) + f2 * M.get( 8) + f3 * M.get(12));
		r.set( 5, f0 * M.get( 1) + f1 * M.get(5) + f2 * M.get( 9) + f3 * M.get(13));
		r.set( 6, f0 * M.get( 2) + f1 * M.get(6) + f2 * M.get(10) + f3 * M.get(14));
		r.set( 7, f0 * M.get( 3) + f1 * M.get(7) + f2 * M.get(11) + f3 * M.get(15));
		f0 = m.get(8); f1 = m.get(9); f2 = m.get(10); f3 = m.get(11);
		r.set( 8, f0 * M.get( 0) + f1 * M.get(4) + f2 * M.get( 8) + f3 * M.get(12));
		r.set( 9, f0 * M.get( 1) + f1 * M.get(5) + f2 * M.get( 9) + f3 * M.get(13));
		r.set(10, f0 * M.get( 2) + f1 * M.get(6) + f2 * M.get(10) + f3 * M.get(14));
		r.set(11, f0 * M.get( 3) + f1 * M.get(7) + f2 * M.get(11) + f3 * M.get(15));
		f0 = m.get(12); f1 = m.get(13); f2 = m.get(14); f3 = m.get(15);
		r.set(12, f0 * M.get( 0) + f1 * M.get(4) + f2 * M.get( 8) + f3 * M.get(12));
		r.set(13, f0 * M.get( 1) + f1 * M.get(5) + f2 * M.get( 9) + f3 * M.get(13));
		r.set(14, f0 * M.get( 2) + f1 * M.get(6) + f2 * M.get(10) + f3 * M.get(14));
		r.set(15, f0 * M.get( 3) + f1 * M.get(7) + f2 * M.get(11) + f3 * M.get(15));
		//@formatter:on
	}

	// | 0 1 2 3 | 0 1 2 3 |
	//           | 4 5 6 7 |
	//           | 8 9 A B | v(vec4) * m(mat4) => r(vec4)
	//           | C D E F | r[0] = dot(v[0,1,2,3], m[0,4,8,C]); ...
	public static void matVecMulJava(@NotNull Vec4 v, @NotNull Mat4 m, @NotNull Vec4 r) {
		float v0 = v.get(0), v1 = v.get(1), v2 = v.get(2), v3 = v.get(3);
		//@formatter:off
		r.set(0, v0 * m.get(0) + v1 * m.get(4) + v2 * m.get( 8) + v3 * m.get(12));
		r.set(1, v0 * m.get(1) + v1 * m.get(5) + v2 * m.get( 9) + v3 * m.get(13));
		r.set(2, v0 * m.get(2) + v1 * m.get(6) + v2 * m.get(10) + v3 * m.get(14));
		r.set(3, v0 * m.get(3) + v1 * m.get(7) + v2 * m.get(11) + v3 * m.get(15));
		//@formatter:on
	}

	static void main0(@SuppressWarnings("unused") @NotNull String @NotNull [] args) {
		var m0 = new Mat4().identity();
		var m1 = new Mat4().identity();
		var m2 = new Mat4().identity();
		var m3 = new Mat4().identity();
		m0
				.setRow(0, 2, 3, 4, 5)
				.setRow(1, 6, 7, 8, 9)
				.setRow(2, 20, 30, 40, 50)
				.setRow(3, 60, 70, 80, 90);

		m1
				.setRow(0, 200, 300, 400, 500)
				.setRow(1, 600, 700, 800, 900)
				.setRow(2, 2000, 3000, 4000, 5000)
				.setRow(3, 6000, 7000, 8000, 9000);

		m2.mul(m0, m1);
		matMulJava(m0, m1, m3);
		printMat4(m2);
		printMat4(m3);
		System.out.println(equalsMat4(m2, m3));

		var v0 = new Vec4().set(100, 200, 300, 400);
		var v1 = new Vec4().zero();
		matVecMulJava(v0, m0, v1);
		v0.mul(m0);
		printMat4(m0);
		printVec4(v0);
		printVec4(v1);
		System.out.println(equalsVec4(v0, v1));

		var m = new Mat4().lookAt(
				-1, -1, -1,
				0, 0, 1,
				0, 1, 0);
		System.out.println(m);
		var v = new Vec4().set(1, 1, 1).mul(m);
		System.out.println(v);

//		//noinspection InfiniteLoopStatement
//		for (; ; ) {
		v = v.set(1, 1, 50).mul(m.ortho(10, 10, 0, 100));
		System.out.println(v);

		v = v.set(1, 1, 50).mul(m.frustum(90, 9f / 16, 1, 100));
		System.out.println(v);

//			//noinspection BusyWait
//			Thread.sleep(1000);
//		}

		m.transform(null, 0, 0, 0, 10, 0, 0, 1, 1, 1);
		System.out.println(v.set(1, 0, 0).mul(m));
		System.out.println(v.set(0, 1, 0).mul(m));
		System.out.println(v.set(0, 0, 1).mul(m));
		m.transform(null, 0, 0, 0, 0, 10, 0, 1, 1, 1);
		System.out.println(v.set(1, 0, 0).mul(m));
		System.out.println(v.set(0, 1, 0).mul(m));
		System.out.println(v.set(0, 0, 1).mul(m));
		m.transform(null, 0, 0, 0, 0, 0, 10, 1, 1, 1);
		System.out.println(v.set(1, 0, 0).mul(m));
		System.out.println(v.set(0, 1, 0).mul(m));
		System.out.println(v.set(0, 0, 1).mul(m));

		System.out.println(System.getProperty("os.name"));
		for (int i = 0; i < 10; i++)
			System.out.format("%X\n", UnsafeUtil.allocateMemoryPage(0x10000));
	}

	static void main(@SuppressWarnings("unused") @NotNull String @NotNull [] args) throws Throwable {
		var src = Files.readAllBytes(Path.of("res/demo.opa"));
		var pSrc = UnsafeUtil.allocateMemory(src.length);
		UnsafeUtil.copyMemory(src, pSrc);
		var tmp = UnsafeUtil.allocateMemory(16);
		var t = System.nanoTime();
		int r = AudioMini.decodeOpusToWaveFile(pSrc, src.length, tmp, tmp + 8);
		System.out.println("decodeOpusToWaveFile = " + r + ", " + (System.nanoTime() - t) / 1_000_000);
		if (r == 0) {
			try (var fos = new FileOutputStream("~$test.wav")) {
				int n = fos.getChannel().write(MemoryUtil.memByteBuffer(UnsafeUtil.getLong(tmp), (int)UnsafeUtil.getLong(tmp + 8)));
				System.out.println("write = " + n);
			}
		}
	}

	static void main3(@SuppressWarnings("unused") @NotNull String @NotNull [] args) throws InterruptedException {
		var a = new AudioMini();
		a.init();
		var mf = ResourceMapFile.loadMapFile("res/demo.opa");
		assert mf != null;
		Logger.info("loading ...");
		var s = a.loadSound(MemoryUtil.memAddress(mf), mf.remaining(), true);
		Logger.info("playing ... " + s.getSampleRate() + ", " + s.getTotalFrames());
		s.setPos(6_000_000);
		s.resume();
		for (int i = 0; i < 1000; i++) {
			Logger.info("pos={}", s.getPos());
			Thread.sleep(1000);
		}
	}

	// -Dstdout.encoding=utf-8
	static void main4(@SuppressWarnings("unused") @NotNull String @NotNull [] args) {
		final int pixelSize = 32;
		final int codePoint = '一';

		var mbb = Objects.requireNonNull(ResourceMapFile.loadMapFile("res/LXGWWenKaiGB-Regular.ttf"));
		var fontInfoPtr = UnsafeUtil.allocateMemory(STBTTFontinfo.SIZEOF);

		var bmpBufPtr = UnsafeUtil.allocateMemory((long)pixelSize * pixelSize);
		if (nstbtt_InitFont(fontInfoPtr, MemoryUtil.memAddress(mbb), 0) == 0)
			throw new AssertionError();
		var scale = nstbtt_ScaleForPixelHeight(fontInfoPtr, pixelSize);
		var tmpBuf = Engine.current().getTmpBuf();
		nstbtt_GetFontVMetrics(fontInfoPtr, tmpBuf.ptr(), tmpBuf.ptr(4), tmpBuf.ptr(8));
		int ascent = tmpBuf.getInt();
		int descent = tmpBuf.getInt(1);
		int lineGap = tmpBuf.getInt(2);
		int baseline = Math.round(ascent * scale);

		nstbtt_GetCodepointHMetrics(fontInfoPtr, codePoint, tmpBuf.ptr(), tmpBuf.ptr(4));
		int advanceX = tmpBuf.getInt();
		int leftSideBearing = tmpBuf.getInt(1);
		nstbtt_GetCodepointBitmapBox(fontInfoPtr, codePoint, scale, scale,
				tmpBuf.ptr(), tmpBuf.ptr(4), tmpBuf.ptr(8), tmpBuf.ptr(12));
		int x0 = tmpBuf.getInt();
		int y0 = tmpBuf.getInt(1);
		int x1 = tmpBuf.getInt(2);
		int y1 = tmpBuf.getInt(3);
		int w = Math.min(x1 - x0, pixelSize);
		int h = Math.min(y1 - y0, pixelSize);
		nstbtt_MakeCodepointBitmap(fontInfoPtr, bmpBufPtr, w, h, w, scale, scale, codePoint);
		// int adv = Math.round((advanceX + nstbtt_GetCodepointKernAdvance(fontInfoPtr, codePoint, nextCodePoint)) * scale);

		System.out.println("scale   : " + scale);
		System.out.println("ascent  : " + ascent);
		System.out.println("descent : " + descent);
		System.out.println("lineGap : " + lineGap);
		System.out.println("baseline: " + baseline);
		System.out.println((char)codePoint + ": adv=" + advanceX * scale + ", lsb=" + leftSideBearing * scale
						   + ", x=" + x0 + "~" + x1 + ", " + "y=" + y0 + "~" + y1);
		System.out.println("top=" + (baseline + y0) + ", left=" + x0 + ", w=" + w + ", h=" + h);
		for (int i = 0, y = 0; y < h; y++) {
			for (int x = 0; x < w; x++, i++)
				System.out.format(" %2X", UnsafeUtil.getByte(bmpBufPtr + i));
			System.out.println();
		}
		System.out.println("end");
	}

	static final boolean usePBO = true;

	static void main5(@SuppressWarnings("unused") @NotNull String @NotNull [] args) throws Exception {
		var initParams = new Render.InitParams();
		initParams.title = "TestJoe";
		initParams.fullWindow = false;
		var engine = Engine.current();
		try {
			engine.init(new RenderGles3(), initParams, new InputGlfw(), new AudioMini());
			engine.getResourceManager().recollectAll("res");
			var render = engine.getRender();
/*
			System.out.println("GL_PACK_ROW_LENGTH:" + GLES20.glGetInteger(GLES30.GL_PACK_ROW_LENGTH));
			System.out.println("GL_PACK_SKIP_ROWS:" + GLES20.glGetInteger(GLES30.GL_PACK_SKIP_ROWS));
			System.out.println("GL_PACK_SKIP_PIXELS:" + GLES20.glGetInteger(GLES30.GL_PACK_SKIP_PIXELS));
			System.out.println("GL_PACK_ALIGNMENT:" + GLES20.glGetInteger(GLES30.GL_PACK_ALIGNMENT));
			System.out.println("GL_UNPACK_ROW_LENGTH:" + GLES20.glGetInteger(GLES30.GL_UNPACK_ROW_LENGTH));
			System.out.println("GL_UNPACK_IMAGE_HEIGHT:" + GLES20.glGetInteger(GLES30.GL_UNPACK_IMAGE_HEIGHT));
			System.out.println("GL_UNPACK_SKIP_ROWS:" + GLES20.glGetInteger(GLES30.GL_UNPACK_SKIP_ROWS));
			System.out.println("GL_UNPACK_SKIP_PIXELS:" + GLES20.glGetInteger(GLES30.GL_UNPACK_SKIP_PIXELS));
			System.out.println("GL_UNPACK_SKIP_IMAGES:" + GLES20.glGetInteger(GLES30.GL_UNPACK_SKIP_IMAGES));
			System.out.println("GL_UNPACK_ALIGNMENT:" + GLES20.glGetInteger(GLES30.GL_UNPACK_ALIGNMENT));

			var t = render.createTexture2D(16,16,1,0,0);
			var dataPtr = UnsafeUtil.allocateMemory(256);
			for (int i = 0; i < 256;i ++)
				UnsafeUtil.putByte(dataPtr + i, (byte)i);
			t.putData(0,0,13,15,dataPtr);
			UnsafeUtil.zeroMemory(dataPtr, 256);
			var frame = render.createFrame();
			frame.bindTexture(0, t);
			render.readFrame(frame, 0, 1, 0, 0, 16, 16, dataPtr);
			for (int i = 0; i < 256;i ++) {
				System.out.format(" %02X", UnsafeUtil.getByte(dataPtr + i));
				if (i % 16 == 15)
					System.out.println();
			}
*/
			var font = engine.getResourceManager().addFont("default", "LXGWWenKaiGB-Regular.ttf", 32, 4096);
			var tex = font.getTexture();
			var texW = tex.width();
			var texH = tex.height();
			var dataSize = (long)texW * texH;
			var tb = font.createTextBox(16);
			tb.set("测aim01 ~*试", 512, 2);
			System.out.println("TextBox: w=" + tb.width() + ", h=" + tb.height() + ", vn=" + tb.vertexCount()
							   + ", mesh.size=" + tb.mesh().getVertexBuffer().dataSize() + ", limited=" + tb.limited());
			var frame = render.createFrame();
			frame.bindTexture(0, tex);

			var t = System.nanoTime();
			System.out.println("---1: " + t);
			if (usePBO) {
				var pbo = render.createDownloadPixelBuffer(dataSize); // 1ms
				System.out.println("---2: " + System.nanoTime());
				render.readFrame(frame, 0, 1, 0, 0, texW, texH, pbo); // 0ms
				System.out.println("---3: " + System.nanoTime());
				pbo.download(0, dataSize, dataPtr -> { // 1ms
					System.out.println("---4: " + System.nanoTime());
					try (var fos = new FileOutputStream("~$font.tga")) { // 6ms
						final var head = new byte[18];
						head[2] = 3; // 2:24/32-bit; 3:8-bit
						head[12] = (byte)texW;
						head[13] = (byte)(texW >> 8);
						head[14] = (byte)texH;
						head[15] = (byte)(texH >> 8);
						head[16] = 8; // bits
						head[17] = 0x20; // 0:bottom=>top; 0x20:from top=>bottom
						fos.write(head);
						int n = fos.getChannel().write(MemoryUtil.memByteBuffer(dataPtr, (int)dataSize));
						System.out.println("---5: " + System.nanoTime());
						System.out.println("write=" + n);
					} catch (Exception e) {
						UnsafeUtil.forceThrow(e);
					}
				});
				System.out.println("---6: " + System.nanoTime());
			} else {
				var dataPtr = UnsafeUtil.allocateMemory(dataSize);
				System.out.println("---2: " + System.nanoTime());
				render.readFrame(frame, 0, 1, 0, 0, texW, texH, dataPtr); // 4ms
				System.out.println("---3: " + System.nanoTime());
				try (var fos = new FileOutputStream("~$font.tga")) { // 5ms
					final var head = new byte[18];
					head[2] = 3; // 2:24/32-bit; 3:8-bit
					head[12] = (byte)texW;
					head[13] = (byte)(texW >> 8);
					head[14] = (byte)texH;
					head[15] = (byte)(texH >> 8);
					head[16] = 8; // bits
					head[17] = 0x20; // 0:bottom=>top; 0x20:from top=>bottom
					fos.write(head);
					int n = fos.getChannel().write(MemoryUtil.memByteBuffer(dataPtr, (int)dataSize));
					System.out.println("---5: " + System.nanoTime());
					System.out.println("write=" + n);
				}
				System.out.println("---6: " + System.nanoTime());
			}
			System.out.println((System.nanoTime() - t) + " ns");
		} finally {
			engine.destroy();
		}
		System.out.println("end");
	}

	static void main6(@SuppressWarnings("unused") @NotNull String @NotNull [] args) {
		var mat = ResourceVideo.Image.createCoeffMat(ResourceVideo.Image.MAT_COEFF_UNSPECIFIED, true);
		var vec = new Vec4();
		System.out.println(vec.set(0, 128f / 255, 128f / 255).mul(mat));
		System.out.println(vec.set(1, 128f / 255, 128f / 255).mul(mat));
		System.out.println(vec.set(128f / 255, 128f / 255, 128f / 255).mul(mat));
		System.out.println(vec.set(0, 0, 0).mul(mat));
		System.out.println(vec.set(0, 1, 1).mul(mat));
		System.out.println(vec.set(128f / 255, 0, 0).mul(mat));
		System.out.println(vec.set(128f / 255, 1, 1).mul(mat));
		System.out.println(vec.set(1, 0, 0).mul(mat));
		System.out.println(vec.set(1, 1, 1).mul(mat));
	}

	static void main7(@SuppressWarnings("unused") @NotNull String @NotNull [] args) {
		var v = new ResourceVideo("res/demo.avifs", 0, -1);
		System.out.println("frameCount = " + v.getFrameCount());
		System.out.println("duration   = " + v.getDuration());
		System.out.println("hasAlpha   = " + v.hasAlpha());
		var t = System.nanoTime();
		for (int i = 0, n = v.getFrameCount(); i < n; i++) {
			var img = v.decodeNextImage();
			System.out.format("%6d:%8.3f+%.3f: %dx%dx%d yuvFmt=%d,fulRng=%d,samPos=%d,preMul=%d,matCoe=%d %X(+%d) %X(+%d) %X(+%d) %X(+%d)\n",
					v.getCurFrameIdx(), v.getCurFrameTime(), v.getCurFrameDuration(),
					img.width, img.height, img.depth, img.yuvFormat, img.yuvFullRange ? 1 : 0,
					img.samplePos, img.alphaPreMultiplied ? 1 : 0, img.matrixCoefficients,
					img.yData, img.yStride, img.uData, img.uStride, img.vData, img.vStride, img.aData, img.aStride);
		}
		System.out.println("end! " + (System.nanoTime() - t) / 1_000_000 + " ms");
	}
}
